package io.agora.api.example.examples.advanced;

import static io.agora.api.example.common.model.Examples.ADVANCED;
import static io.agora.rtc2.Constants.AUDIO_EFFECT_OFF;
import static io.agora.rtc2.Constants.CHAT_BEAUTIFIER_FRESH;
import static io.agora.rtc2.Constants.CHAT_BEAUTIFIER_MAGNETIC;
import static io.agora.rtc2.Constants.CHAT_BEAUTIFIER_VITALITY;
import static io.agora.rtc2.Constants.PITCH_CORRECTION;
import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_3D_VOICE;
import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_ETHEREAL;
import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_KTV;
import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_PHONOGRAPH;
import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_SPACIAL;
import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_STUDIO;
import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_VIRTUAL_STEREO;
import static io.agora.rtc2.Constants.ROOM_ACOUSTICS_VOCAL_CONCERT;
import static io.agora.rtc2.Constants.STYLE_TRANSFORMATION_POPULAR;
import static io.agora.rtc2.Constants.STYLE_TRANSFORMATION_RNB;
import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_CLEAR;
import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_DEEP;
import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_FALSETTO;
import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_FULL;
import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_MELLOW;
import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_RESOUNDING;
import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_RINGING;
import static io.agora.rtc2.Constants.TIMBRE_TRANSFORMATION_VIGOROUS;
import static io.agora.rtc2.Constants.ULTRA_HIGH_QUALITY_VOICE;
import static io.agora.rtc2.Constants.VOICE_BEAUTIFIER_OFF;
import static io.agora.rtc2.Constants.VOICE_CHANGER_BASS;
import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_BOY;
import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_GIRL;
import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_HULK;
import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_OLDMAN;
import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_PIGKING;
import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_SISTER;
import static io.agora.rtc2.Constants.VOICE_CHANGER_EFFECT_UNCLE;
import static io.agora.rtc2.Constants.VOICE_CHANGER_NEUTRAL;
import static io.agora.rtc2.Constants.VOICE_CHANGER_SOLID;
import static io.agora.rtc2.Constants.VOICE_CHANGER_SWEET;
import static io.agora.rtc2.Constants.VOICE_CONVERSION_OFF;

import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.SeekBar;
import android.widget.Spinner;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.yanzhenjie.permission.AndPermission;
import com.yanzhenjie.permission.runtime.Permission;

import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import io.agora.api.example.MainApplication;
import io.agora.api.example.R;
import io.agora.api.example.annotation.Example;
import io.agora.api.example.common.BaseFragment;
import io.agora.api.example.common.widget.AudioSeatManager;
import io.agora.api.example.utils.CommonUtil;
import io.agora.api.example.utils.TokenUtils;
import io.agora.rtc2.ChannelMediaOptions;
import io.agora.rtc2.Constants;
import io.agora.rtc2.IRtcEngineEventHandler;
import io.agora.rtc2.RtcEngine;
import io.agora.rtc2.RtcEngineConfig;

@Example(
        index = 4,
        group = ADVANCED,
        name = R.string.item_voiceeffects,
        actionId = R.id.action_mainFragment_to_VoiceEffects,
        tipsId = R.string.voiceeffects
)
public class VoiceEffects extends BaseFragment implements View.OnClickListener, AdapterView.OnItemSelectedListener, CompoundButton.OnCheckedChangeListener, SeekBar.OnSeekBarChangeListener {
    private static final String TAG = VoiceEffects.class.getSimpleName();

    private RtcEngine engine;
    private int myUid;
    private boolean joined = false;

    private EditText et_channel;
    private Button join;
    private Spinner audioProfile, audioScenario,
            chatBeautifier, timbreTransformation, voiceChanger, styleTransformation, roomAcoustics, pitchCorrection, _pitchModeOption, _pitchValueOption, voiceConversion,
            customBandFreq, customReverbKey;
    private ViewGroup _voice3DLayout, _pitchModeLayout, _pitchValueLayout;
    private SeekBar _voice3DCircle, customPitch, customBandGain, customReverbValue;

    private AudioSeatManager audioSeatManager;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        handler = new Handler();
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_voice_effects, container, false);
        return view;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        // Join layout
        join = view.findViewById(R.id.btn_join);
        audioProfile = view.findViewById(R.id.audio_profile_spinner);
        audioScenario = view.findViewById(R.id.audio_scenario_spinner);
        et_channel = view.findViewById(R.id.et_channel);

        audioScenario.setOnItemSelectedListener(this);
        join.setOnClickListener(this);

        // Voice Beautifier / Effects Preset layout
        chatBeautifier = view.findViewById(R.id.audio_chat_beautifier);
        timbreTransformation = view.findViewById(R.id.audio_timbre_transformation);
        voiceChanger = view.findViewById(R.id.audio_voice_changer);
        styleTransformation = view.findViewById(R.id.audio_style_transformation);
        roomAcoustics = view.findViewById(R.id.audio_room_acoustics);
        _voice3DLayout = view.findViewById(R.id.audio_3d_voice_layout);
        _voice3DCircle = view.findViewById(R.id.audio_3d_voice_circle);
        pitchCorrection = view.findViewById(R.id.audio_pitch_correction);
        _pitchModeLayout = view.findViewById(R.id.audio_pitch_mode_layout);
        _pitchModeOption = view.findViewById(R.id.audio_pitch_mode_option);
        _pitchValueLayout = view.findViewById(R.id.audio_pitch_value_layout);
        _pitchValueOption = view.findViewById(R.id.audio_pitch_value_option);
        voiceConversion = view.findViewById(R.id.audio_voice_conversion);

        chatBeautifier.setOnItemSelectedListener(this);
        timbreTransformation.setOnItemSelectedListener(this);
        voiceChanger.setOnItemSelectedListener(this);
        styleTransformation.setOnItemSelectedListener(this);
        roomAcoustics.setOnItemSelectedListener(this);
        pitchCorrection.setOnItemSelectedListener(this);
        voiceConversion.setOnItemSelectedListener(this);
        _voice3DCircle.setOnSeekBarChangeListener(this);
        _pitchModeOption.setOnItemSelectedListener(this);
        _pitchValueOption.setOnItemSelectedListener(this);

        // Customize Voice Effects Layout
        customPitch = view.findViewById(R.id.audio_custom_pitch); // engine.setLocalVoicePitch()
        customBandFreq = view.findViewById(R.id.audio_custom_band_freq); // engine.setLocalVoiceEqualization()
        customBandGain = view.findViewById(R.id.audio_custom_band_gain); // engine.setLocalVoiceEqualization()
        customReverbKey = view.findViewById(R.id.audio_custom_reverb_key);
        customReverbValue = view.findViewById(R.id.audio_custom_reverb_value); //engine.setLocalVoiceReverb()

        customPitch.setOnSeekBarChangeListener(this);
        customBandGain.setOnSeekBarChangeListener(this);
        customReverbValue.setOnSeekBarChangeListener(this);
        customBandFreq.setOnItemSelectedListener(this);
        customReverbKey.setOnItemSelectedListener(this);


        audioSeatManager = new AudioSeatManager(
                view.findViewById(R.id.audio_place_01),
                view.findViewById(R.id.audio_place_02)
        );

        resetControlLayoutByJoined();
    }

    private void resetControlLayoutByJoined() {
        audioProfile.setEnabled(!joined);

        chatBeautifier.setEnabled(joined);
        timbreTransformation.setEnabled(joined);
        voiceChanger.setEnabled(joined);
        styleTransformation.setEnabled(joined);
        roomAcoustics.setEnabled(joined);
        _voice3DLayout.setVisibility(View.GONE);
        pitchCorrection.setEnabled(joined);
        _pitchModeLayout.setVisibility(View.GONE);
        _pitchValueLayout.setVisibility(View.GONE);
        voiceConversion.setEnabled(joined);

        customPitch.setEnabled(joined);
        customBandFreq.setEnabled(joined);
        customBandGain.setEnabled(joined);
        customReverbKey.setEnabled(joined);
        customReverbValue.setEnabled(joined);


        chatBeautifier.setSelection(0);
        voiceChanger.setSelection(0);
        timbreTransformation.setSelection(0);
        roomAcoustics.setSelection(0);
        pitchCorrection.setSelection(0);
        voiceConversion.setSelection(0);

        customPitch.setProgress(0);
        customBandGain.setProgress(0);
        customReverbValue.setProgress(0);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // Check if the context is valid
        Context context = getContext();
        if (context == null) {
            return;
        }
        try {
            RtcEngineConfig config = new RtcEngineConfig();
            /**
             * The context of Android Activity
             */
            config.mContext = context.getApplicationContext();
            /**
             * The App ID issued to you by Agora. See <a href="https://docs.agora.io/en/Agora%20Platform/token#get-an-app-id"> How to get the App ID</a>
             */
            config.mAppId = getString(R.string.agora_app_id);
            /** Sets the channel profile of the Agora RtcEngine.
             CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile.
             Use this profile in one-on-one calls or group calls, where all users can talk freely.
             CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast
             channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams;
             an audience can only receive streams.*/
            config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
            /**
             * IRtcEngineEventHandler is an abstract class providing default implementation.
             * The SDK uses this class to report to the app on SDK runtime events.
             */
            config.mEventHandler = iRtcEngineEventHandler;
            config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT);
            config.mAreaCode = ((MainApplication)getActivity().getApplication()).getGlobalSettings().getAreaCode();
            engine = RtcEngine.create(config);
            /**
             * This parameter is for reporting the usages of APIExample to agora background.
             * Generally, it is not necessary for you to set this parameter.
             */
            engine.setParameters("{"
                    + "\"rtc.report_app_scenario\":"
                    + "{"
                    + "\"appScenario\":" + 100 + ","
                    + "\"serviceType\":" + 11 + ","
                    + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
                    + "}"
                    + "}");
            /* setting the local access point if the private cloud ip was set, otherwise the config will be invalid.*/
            engine.setLocalAccessPoint(((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig());
        } catch (Exception e) {
            e.printStackTrace();
            getActivity().onBackPressed();
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        /**leaveChannel and Destroy the RtcEngine instance*/
        if (engine != null) {
            engine.leaveChannel();
        }
        handler.post(RtcEngine::destroy);
        engine = null;
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_join) {
            if (!joined) {
                CommonUtil.hideInputBoard(getActivity(), et_channel);
                // call when join button hit
                String channelId = et_channel.getText().toString();
                // Check permission
                if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) {
                    joinChannel(channelId);
                    return;
                }
                // Request permission
                AndPermission.with(this).runtime().permission(
                        Permission.Group.STORAGE,
                        Permission.Group.MICROPHONE
                ).onGranted(permissions ->
                {
                    // Permissions Granted
                    joinChannel(channelId);
                }).start();
            } else {
                joined = false;
                resetControlLayoutByJoined();
                /**After joining a channel, the user must call the leaveChannel method to end the
                 * call before joining another channel. This method returns 0 if the user leaves the
                 * channel and releases all resources related to the call. This method call is
                 * asynchronous, and the user has not exited the channel when the method call returns.
                 * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback.
                 * A successful leaveChannel method call triggers the following callbacks:
                 *      1:The local client: onLeaveChannel.
                 *      2:The remote client: onUserOffline, if the user leaving the channel is in the
                 *          Communication channel, or is a BROADCASTER in the Live Broadcast profile.
                 * @returns 0: Success.
                 *          < 0: Failure.
                 * PS:
                 *      1:If you call the destroy method immediately after calling the leaveChannel
                 *          method, the leaveChannel process interrupts, and the SDK does not trigger
                 *          the onLeaveChannel callback.
                 *      2:If you call the leaveChannel method during CDN live streaming, the SDK
                 *          triggers the removeInjectStreamUrl method.*/
                engine.leaveChannel();
                join.setText(getString(R.string.join));
                audioSeatManager.downAllSeats();
            }
        }
    }

    private int getPitch1Value(String str) {
        switch (str) {
            case "Natural Minor":
                return 2;
            case "Breeze Minor":
                return 3;
            default:
                return 1;
        }
    }

    private int getPitch2Value(String str) {
        switch (str) {
            case "A Pitch":
                return 1;
            case "A# Pitch":
                return 2;
            case "B Pitch":
                return 3;
            case "C# Pitch":
                return 5;
            case "D Pitch":
                return 6;
            case "D# Pitch":
                return 7;
            case "E Pitch":
                return 8;
            case "F Pitch":
                return 9;
            case "F# Pitch":
                return 10;
            case "G Pitch":
                return 11;
            case "G# Pitch":
                return 12;
            default:
                return 4;
        }
    }

    /**
     * @param channelId Specify the channel name that you want to join.
     *                  Users that input the same channel name join the same channel.
     */
    private void joinChannel(String channelId) {
        /**In the demo, the default is to enter as the anchor.*/
        engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
        // audio config
        engine.setAudioProfile(
                Constants.AudioProfile.valueOf(audioProfile.getSelectedItem().toString()).ordinal(),
                Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString()).ordinal()
        );

        /**Please configure accessToken in the string_config file.
         * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see
         *      https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token
         * A token generated at the server. This applies to scenarios with high-security requirements. For details, see
         *      https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/
        TokenUtils.gen(requireContext(), channelId, 0, accessToken -> {
            /** Allows a user to join a channel.
             if you do not specify the uid, we will generate the uid for you*/

            ChannelMediaOptions option = new ChannelMediaOptions();
            option.autoSubscribeAudio = true;
            option.autoSubscribeVideo = true;
            int res = engine.joinChannel(accessToken, channelId, 0, option);
            if (res != 0) {
                // Usually happens with invalid parameters
                // Error code description can be found at:
                // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
                // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html
                showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
                Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res)));
                return;
            }
            // Prevent repeated entry
            join.setEnabled(false);
        });
    }

    /**
     * IRtcEngineEventHandler is an abstract class providing default implementation.
     * The SDK uses this class to report to the app on SDK runtime events.
     */
    private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
        /**
         * Error code description can be found at:
         * en: https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
         * cn: https://docs.agora.io/cn/video-call-4.x/API%20Reference/java_ng/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onerror
         */
        @Override
        public void onError(int err) {
            Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
            showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err)));
        }

        /**Occurs when a user leaves the channel.
         * @param stats With this callback, the application retrieves the channel information,
         *              such as the call duration and statistics.*/
        @Override
        public void onLeaveChannel(RtcStats stats) {
            super.onLeaveChannel(stats);
            Log.i(TAG, String.format("local user %d leaveChannel!", myUid));
            showLongToast(String.format("local user %d leaveChannel!", myUid));
        }

        /**Occurs when the local user joins a specified channel.
         * The channel name assignment is based on channelName specified in the joinChannel method.
         * If the uid is not specified when joinChannel is called, the server automatically assigns a uid.
         * @param channel Channel name
         * @param uid User ID
         * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/
        @Override
        public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
            Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
            showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
            myUid = uid;
            joined = true;
            handler.post(new Runnable() {
                @Override
                public void run() {
                    join.setEnabled(true);
                    join.setText(getString(R.string.leave));
                    resetControlLayoutByJoined();
                    audioSeatManager.upLocalSeat(uid);
                }
            });
        }

        /**Since v2.9.0.
         * This callback indicates the state change of the remote audio stream.
         * PS: This callback does not work properly when the number of users (in the Communication profile) or
         *     broadcasters (in the Live-broadcast profile) in the channel exceeds 17.
         * @param uid ID of the user whose audio state changes.
         * @param state State of the remote audio
         *   REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due
         *              to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5),
         *              or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7).
         *   REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received.
         *   REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally,
         *              probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2),
         *              REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6).
         *   REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to
         *              REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1).
         *   REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to
         *              REMOTE_AUDIO_REASON_INTERNAL(0).
         * @param reason The reason of the remote audio state change.
         *   REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons.
         *   REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion.
         *   REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery.
         *   REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio
         *               stream or disables the audio module.
         *   REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio
         *              stream or enables the audio module.
         *   REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or
         *               disables the audio module.
         *   REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream
         *              or enables the audio module.
         *   REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel.
         *   @param elapsed Time elapsed (ms) from the local user calling the joinChannel method
         *                  until the SDK triggers this callback.*/
        @Override
        public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) {
            super.onRemoteAudioStateChanged(uid, state, reason, elapsed);
            Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason);
        }

        /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel.
         * @param uid ID of the user whose audio state changes.
         * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole
         *                until this callback is triggered.*/
        @Override
        public void onUserJoined(int uid, int elapsed) {
            super.onUserJoined(uid, elapsed);
            Log.i(TAG, "onUserJoined->" + uid);
            showLongToast(String.format("user %d joined!", uid));
            runOnUIThread(() -> audioSeatManager.upRemoteSeat(uid));
        }

        /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel.
         * @param uid ID of the user whose audio state changes.
         * @param reason Reason why the user goes offline:
         *   USER_OFFLINE_QUIT(0): The user left the current channel.
         *   USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data
         *              packet was received within a certain period of time. If a user quits the
         *               call and the message is not passed to the SDK (due to an unreliable channel),
         *               the SDK assumes the user dropped offline.
         *   USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from
         *               the host to the audience.*/
        @Override
        public void onUserOffline(int uid, int reason) {
            Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason));
            showLongToast(String.format("user %d offline! reason:%d", uid, reason));
            runOnUIThread(() -> audioSeatManager.downSeat(uid));
        }

        @Override
        public void onLocalAudioStats(LocalAudioStats stats) {
            super.onLocalAudioStats(stats);
            runOnUIThread(() -> {
                Map<String, String> _stats = new LinkedHashMap<>();
                _stats.put("sentSampleRate", stats.sentSampleRate + "");
                _stats.put("sentBitrate", stats.sentBitrate + " kbps");
                _stats.put("internalCodec", stats.internalCodec + "");
                _stats.put("audioDeviceDelay", stats.audioDeviceDelay + " ms");
                audioSeatManager.getLocalSeat().updateStats(_stats);
            });
        }

        @Override
        public void onRemoteAudioStats(RemoteAudioStats stats) {
            super.onRemoteAudioStats(stats);
            runOnUIThread(() -> {
                Map<String, String> _stats = new LinkedHashMap<>();
                _stats.put("numChannels", stats.numChannels + "");
                _stats.put("receivedBitrate", stats.receivedBitrate + " kbps");
                _stats.put("audioLossRate", stats.audioLossRate + "");
                _stats.put("jitterBufferDelay", stats.jitterBufferDelay + " ms");
                audioSeatManager.getRemoteSeat(stats.uid).updateStats(_stats);
            });
        }
    };

    @Override
    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        if (parent.getTag() != null) {
            parent.setTag(null);
            return;
        }

        if (parent == audioScenario) {
            engine.setAudioScenario(Constants.AudioScenario.valueOf(audioScenario.getSelectedItem().toString()).ordinal());
            return;
        }

        // Voice Beautifier / Effects Preset layout
        List<Spinner> voiceBeautifierSpinner = Arrays.asList(chatBeautifier, timbreTransformation);
        if (voiceBeautifierSpinner.contains(parent)) {
            String item = parent.getSelectedItem().toString();
            int voiceBeautifierValue = getVoiceBeautifierValue(item);
            engine.setVoiceBeautifierPreset(voiceBeautifierValue);

            for (Spinner spinner : voiceBeautifierSpinner) {
                if (spinner != parent) {
                    if(spinner.getSelectedItemPosition() != 0){
                        spinner.setTag("reset");
                        spinner.setSelection(0);
                    }
                }
            }
            return;
        }

        List<Spinner> audioEffectSpinner = Arrays.asList(voiceChanger, styleTransformation, roomAcoustics, pitchCorrection);
        if (audioEffectSpinner.contains(parent)) {
            String item = parent.getSelectedItem().toString();
            int audioEffectPreset = getAudioEffectPreset(item);
            engine.setAudioEffectPreset(audioEffectPreset);

            for (Spinner spinner : audioEffectSpinner) {
                if (spinner != parent) {
                    if(spinner.getSelectedItemPosition() != 0){
                        spinner.setTag("reset");
                        spinner.setSelection(0);
                    }
                }
            }

            _voice3DLayout.setVisibility(audioEffectPreset == ROOM_ACOUSTICS_3D_VOICE ? View.VISIBLE: View.GONE);
            _pitchModeLayout.setVisibility(audioEffectPreset == PITCH_CORRECTION ? View.VISIBLE : View.GONE);
            _pitchValueLayout.setVisibility(audioEffectPreset == PITCH_CORRECTION ? View.VISIBLE : View.GONE);
            return;
        }

        if(parent == voiceConversion){
            String item = parent.getSelectedItem().toString();
            engine.setVoiceConversionPreset(getVoiceConversionValue(item));
            return;
        }

        if(parent == _pitchModeOption || parent == _pitchValueOption){
            int effectOption1 = getPitch1Value(_pitchModeOption.getSelectedItem().toString());
            int effectOption2 = getPitch2Value(_pitchValueOption.getSelectedItem().toString());
            engine.setAudioEffectParameters(PITCH_CORRECTION, effectOption1, effectOption2);
        }
    }

    private int getVoiceConversionValue(String label) {
        switch (label) {
            case "VOICE_CHANGER_NEUTRAL":
                return VOICE_CHANGER_NEUTRAL;
            case "VOICE_CHANGER_SWEET":
                return VOICE_CHANGER_SWEET;
            case "VOICE_CHANGER_SOLID":
                return VOICE_CHANGER_SOLID;
            case "VOICE_CHANGER_BASS":
                return VOICE_CHANGER_BASS;
            case "VOICE_CONVERSION_OFF":
            default:
                return VOICE_CONVERSION_OFF;
        }
    }

    private int getVoiceBeautifierValue(String label) {
        int value;
        switch (label) {
            case "CHAT_BEAUTIFIER_MAGNETIC":
                value = CHAT_BEAUTIFIER_MAGNETIC;
                break;
            case "CHAT_BEAUTIFIER_FRESH":
                value = CHAT_BEAUTIFIER_FRESH;
                break;
            case "CHAT_BEAUTIFIER_VITALITY":
                value = CHAT_BEAUTIFIER_VITALITY;
                break;
            case "TIMBRE_TRANSFORMATION_VIGOROUS":
                value = TIMBRE_TRANSFORMATION_VIGOROUS;
                break;
            case "TIMBRE_TRANSFORMATION_DEEP":
                value = TIMBRE_TRANSFORMATION_DEEP;
                break;
            case "TIMBRE_TRANSFORMATION_MELLOW":
                value = TIMBRE_TRANSFORMATION_MELLOW;
                break;
            case "TIMBRE_TRANSFORMATION_FALSETTO":
                value = TIMBRE_TRANSFORMATION_FALSETTO;
                break;
            case "TIMBRE_TRANSFORMATION_FULL":
                value = TIMBRE_TRANSFORMATION_FULL;
                break;
            case "TIMBRE_TRANSFORMATION_CLEAR":
                value = TIMBRE_TRANSFORMATION_CLEAR;
                break;
            case "TIMBRE_TRANSFORMATION_RESOUNDING":
                value = TIMBRE_TRANSFORMATION_RESOUNDING;
                break;
            case "TIMBRE_TRANSFORMATION_RINGING":
                value = TIMBRE_TRANSFORMATION_RINGING;
                break;
            case "ULTRA_HIGH_QUALITY_VOICE":
                value = ULTRA_HIGH_QUALITY_VOICE;
                break;
            default:
                value = VOICE_BEAUTIFIER_OFF;
        }
        return value;
    }

    private int getAudioEffectPreset(String label) {
        int value;
        switch (label) {
            case "ROOM_ACOUSTICS_KTV":
                value = ROOM_ACOUSTICS_KTV;
                break;
            case "ROOM_ACOUSTICS_VOCAL_CONCERT":
                value = ROOM_ACOUSTICS_VOCAL_CONCERT;
                break;
            case "ROOM_ACOUSTICS_STUDIO":
                value = ROOM_ACOUSTICS_STUDIO;
                break;
            case "ROOM_ACOUSTICS_PHONOGRAPH":
                value = ROOM_ACOUSTICS_PHONOGRAPH;
                break;
            case "ROOM_ACOUSTICS_VIRTUAL_STEREO":
                value = ROOM_ACOUSTICS_VIRTUAL_STEREO;
                break;
            case "ROOM_ACOUSTICS_SPACIAL":
                value = ROOM_ACOUSTICS_SPACIAL;
                break;
            case "ROOM_ACOUSTICS_ETHEREAL":
                value = ROOM_ACOUSTICS_ETHEREAL;
                break;
            case "ROOM_ACOUSTICS_3D_VOICE":
                value = ROOM_ACOUSTICS_3D_VOICE;
                break;
            case "VOICE_CHANGER_EFFECT_UNCLE":
                value = VOICE_CHANGER_EFFECT_UNCLE;
                break;
            case "VOICE_CHANGER_EFFECT_OLDMAN":
                value = VOICE_CHANGER_EFFECT_OLDMAN;
                break;
            case "VOICE_CHANGER_EFFECT_BOY":
                value = VOICE_CHANGER_EFFECT_BOY;
                break;
            case "VOICE_CHANGER_EFFECT_SISTER":
                value = VOICE_CHANGER_EFFECT_SISTER;
                break;
            case "VOICE_CHANGER_EFFECT_GIRL":
                value = VOICE_CHANGER_EFFECT_GIRL;
                break;
            case "VOICE_CHANGER_EFFECT_PIGKING":
                value = VOICE_CHANGER_EFFECT_PIGKING;
                break;
            case "VOICE_CHANGER_EFFECT_HULK":
                value = VOICE_CHANGER_EFFECT_HULK;
                break;
            case "STYLE_TRANSFORMATION_RNB":
                value = STYLE_TRANSFORMATION_RNB;
                break;
            case "STYLE_TRANSFORMATION_POPULAR":
                value = STYLE_TRANSFORMATION_POPULAR;
                break;
            case "PITCH_CORRECTION":
                value = PITCH_CORRECTION;
                break;
            default:
                value = AUDIO_EFFECT_OFF;
        }
        return value;
    }

    @Override
    public void onNothingSelected(AdapterView<?> parent) {

    }

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

    }

    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        if(seekBar == _voice3DCircle){
            int cicle = (int) (1 + 59 * progress * 1.0f / seekBar.getMax());
            // [1,60], 10 default
            engine.setAudioEffectParameters(ROOM_ACOUSTICS_3D_VOICE, cicle, 0);
        }else if(seekBar == customPitch){
            double pitch = 0.5 + 1.5 * progress * 1.0f / seekBar.getMax();
            // pitch: [0.5,2.0], 1.0 default
            engine.setLocalVoicePitch(pitch);
        } else if (seekBar == customBandGain) {
            int value = (int) (-15 + 30 * progress * 1.0f / seekBar.getMax());
            //  [-15,15], 0 default
            engine.setLocalVoiceEqualization(Constants.AUDIO_EQUALIZATION_BAND_FREQUENCY.valueOf(customBandFreq.getSelectedItem().toString()), value);
        } else if (seekBar == customReverbValue) {
            Constants.AUDIO_REVERB_TYPE reverbKey = Constants.AUDIO_REVERB_TYPE.valueOf(customReverbKey.getSelectedItem().toString());
            int value;
            // AUDIO_REVERB_DRY_LEVEL(0)：dry signal， [-20, 10] dB
            // AUDIO_REVERB_WET_LEVEL(1)：wet signal， [-20, 10] dB
            // AUDIO_REVERB_ROOM_SIZE(2)：[0, 100] dB
            // AUDIO_REVERB_WET_DELAY(3)：Wet signal,  [0, 200] ms
            // AUDIO_REVERB_STRENGTH(4)： [0, 100]
            if(reverbKey == Constants.AUDIO_REVERB_TYPE.AUDIO_REVERB_DRY_LEVEL || reverbKey == Constants.AUDIO_REVERB_TYPE.AUDIO_REVERB_WET_LEVEL){
                value = (int) (-20 + 30 * progress * 1.0f / seekBar.getMax());
            }else if(reverbKey == Constants.AUDIO_REVERB_TYPE.AUDIO_REVERB_WET_DELAY){
                value = (int) (200 * progress * 1.0f / seekBar.getMax());
            }else {
                value = (int) (100 * progress * 1.0f / seekBar.getMax());
            }
            engine.setLocalVoiceReverb(reverbKey, value);
        }
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {

    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {

    }
}
